11. 安全性

Grails差不多和Java Servlets一样可靠。然而由于JVM运行代码的特性,Java servlets对一般的缓冲区溢出和恶意URL使用是极为安全和免疫的。

Web安全问题通常由于开发人员的无知过错造成的,Grails提供了一些帮助,可以避免常出现的错误,使安全应用更加容易编写。

Grails可以自动做什么

Grails拥有一些默认的内置安全机制

  1. 所有通过GORM域对象访问标准数据库可以自动避免SQL语句以防止SQL注入攻击。
  2. 默认scaffolding模板HTML文件当打开时所有数据域不显示。
  3. 所有Grails的链接创建标签(link, form, createLink, createLinkTo 等)都使用适当的转义机制以防止代码注入。
  4. Grails提供codecs,运行你在显示HTML,JavaScript和URLs时,转义数据以避免在数据里注入攻击。

11.1 防止攻击

SQL注入

Hibernate是实现GORM域类的基础技术,当提交数据库时会自动转义数据,所以这个没什么问题。然而编写使用未检查的请求参数的脏动态HQL代码,仍然会有问题可能存在。比如如下的这种做法就很容易受HQL注入攻击:

def vulnerable = {
	def books = Book.find("from Book as b where b.title ='" + params.title + "'")
}

千万别这样做。假如你想传递参数,用命名参数和定位参数代替:

def safe = {
	def books = Book.find("from Book as b where b.title =?", [params.title])
}

钓鱼式攻击

这是一个公关关系问题,涉及到避免你的品牌化过程和与顾客设定的沟通手段遭到黑客攻击。顾客需要知道怎么确认收到的emails是真的。

XSS-跨站脚本攻击

你的应用要尽可能多得检验进来的请求是从你的应用里发出的,而不是其它网站。标签和页面流系统能做到这点,Grails对Spring Web Flow的支持也默认包含了这个安全特性。

确保所有呈现到视图的数据值都被转义过也是非常重要的。例如当呈现HTML文件或XHTML文件时,你必须对每个对象调用encodeAsHTML,以便保证用户不会向其他人读取的数据和标签恶意注入JavaScript代码或其他HTML代码。Grails为此目的提供了若干个动态编码方法,因此假如你的输出转义格式没有现成的,你可以很容易得编写自己的编码器。

你也必须避免使用请求参数和数据域来决定用户转向的下一个链接。假如你使用一个successURL参数,在你成功登入之后,用来指示用户的转向;这时攻击者可以通过你的网站模拟登入程序,然后一旦登入就把用户转向到他们的网站,这样就潜在允许JS代码使用该网站的登入帐号。

HTML/URL注入

HTML和URL注入提供有害的数据,之后被用来在页面生成一个链接,点击它不会产生期望的行为,可能会转向另外一个网站或更改请求参数。

Grails提供的codecs可以很容易得处理HTML/URL注入,Grails提供的标签库在适用的地方全都使用encodeAsURL。如果你自己创建能生成链接的标签,你在做的时候要小心。

拒绝服务DoS

负载均衡器和其他应用在这里可能会起到用处,但是还存在其他问题,比如过度查询,攻击者创建一个链接设置结果集的最大值,导致一个查询超过服务器的最大内存限制或拖慢系统运行。解决办法是在请求参数传进动态遍历器或其他GORM查询方法之前,给这些请求参数“消毒”:

def safeMax = Math.max(params.max?.toInteger(), 100) // never let more than 100 results be returned
return Book.list(max:safeMax)

可推测ID号

许多应用把URL的最后一部分当作从GORM或者其他地方获取的某个对象的id。特别是当发生在GORM中时,这些id号是很容易猜测的,因为这些id号通常是一串数字。

因此你必须假定请求用户在请求返回时用请求id号可以看见相对应的对象。

不这样做是隐藏式安全,这样做毫无疑问是非法的,像有letmein的默认密码等等这些情况。

你必须假设每个未受保护URL都可以公共访问。

11.2 编码和解码对象

Grails支持动态编码/解码方法概念。Grails捆绑了一些标准的编解码器,Grails也为开发人员提供了一个贡献自己编解码器的简单机制,这些编解码器在运行时可以被识别。

编解码器类

一个Grails编解码器是个包含一个编码闭包,一个解码闭包或两者皆有。当一个Grails应用启用了,Grails框架会动态从grails-app/utils/目录加载编解码器。

Grails框架将在 grails-app/utils/目录下查找以Codec结尾命名的类名。例如Grails捆绑的其中一个标准编解码器就是HTMLCodec。

假如一个编解码器包含一个encode属性,该属性被赋予一个代码块,Grails会创建一个动态的encode方法,并把该方法添加到Object类,方法名表示了定义encode闭包的编解码器。例如,HTMLCodec类定义了一个编码器代码块,因此Grails会把该闭包与名为encodeAsHTML的Object类相关联。

HTMLCodec类和URLCodec类也定义了解码块,所以Grails会把这些闭包与decodeHTML和decodeURL相关联的。动态编解码器能在Grails应用的任何一个地方执行。例如,考虑一下这种情况,一个报告文件含有一个叫description的属性,该属性包含了需要被转义显示在HTML文档的特殊字符。GSP文档里,一种处理方法就是用如下的动态编码器编码description属性:

${report.description.encodeAsHTML()}

执行解码使用value.decodeHTML()语句。

标准的编解码器

HTMLCodec

编解码器执行HTML转义过程和反转义过程,所以你提供的数值在没有创建任何HTML标签或破坏页面布局下可以被安全得显示出来。例如,给个"Don't you know that 2 > 1?"字符串,你就不能在HTML页面中安全得显示出来,因为大于符号>看起来像要关闭一个标签,特别是你在某个属性内显示这个字符串,情况会更糟糕,像输入框的value属性 。

使用例子如下:

<input name="comment.message" value="${comment.message.encodeAsHTML()}"/>

注意HTML编码不会重新编码单引号或双引号,你必须对属性值只用两个重复引号避免含有引号的正文毁坏你的页面。

URLCodec

当在生成跳转链接,形体处理(form actions)链接,或者任何时候需要数据生成链接时,URL编码是必需的。URL编码可以阻止非法字符串进入链接改变它跳转的目的地,例如"Apple & Blackberry"不能作为get请求中的一个参数,因为&符号为破坏参数解析过程。

使用例子如下:

<a href="/mycontroller/find?searchKey=${lastSearch.encodeAsURL()}">Repeat last search</a>

Base64Codec

执行Base64编码/解码函数,使用例子如下:

Your registration code is: ${user.registrationCode.encodeAsBase64()}

JavaScriptCodec

JavaScriptCodec会转义字符串成为合法的JavaScript字符串,使用例子如下:

Element.update('${elementId}', '${render(template: "/common/message").encodeAsJavaScript()}')

HexCodec

HexCodec会把字节数组或数字数列编码为小写十六进制字符串,可以把十六进制字符串编码为字节数组,使用例子如下:

Selected colour: #${[255,127,255].encodeAsHex()}

MD5Codec

MD5Codec使用MD5算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsMD5()}

MD5BytesCodec

MD5BytesCodec使用MD5算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsMD5Bytes()

SHA1Codec

SHA1Codec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsSHA1()}

SHA1BytesCodec

SHA1BytesCodec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsSHA1Bytes()

SHA256Codec

SHA256Codec使用SHA256算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsSHA256()}

SHA256BytesCodec

SHA256BytesCodec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsSHA256Bytes()

定制编解码器Custom Codecs

许多应用可能定制属于自己的编解码器,Grails在装载标准编解码器时把它们一起装载。定制编解码器类必须在grails-app/utils/目录下定义,而且类名必须以Codec结尾。定制编解码器可能含有一个静态encode块,一个静态decode块或两者皆有。这些编解码代码块需要一个单一参数,当作动态方法操作对象,如下:

class PigLatinCodec {
  static encode = { str ->
    // convert the string to piglatin and return the result
  }
}

在适当的地方,一个应用可以使用上方定义的编解码器做如下的工作:

${lastName.encodeAsPigLatin()}

11.3 认证

尽管现在认证没有默认机制,实际上有上千种方法可以执行认证。然而,用 interceptorsfilters 实施一个简单的认证机制是没意义的。

过滤器运行你对所有的控制器或URI空间应用认证。比如你可以在grails-app/conf/SecurityFilters.groovy类中创建一组新过滤器如下:

class SecurityFilters {
   def filters = {
       loginCheck(controller:'*', action:'*') {
           before = {
              if(!session.user && actionName != "login") {
                  redirect(controller:"user",action:"login")
                  return false					
	           }
           }

} } }

在请求处理执行之前,上述类中的loginCheck过滤器将拦截该执行动作。假如session里没有一个用户而且请求被执行,该login请求处理不是自己转向到自己。

Login请求处理也是很小的:

def login = {
	if(request.get) render(view:"login")
	else {
		def u = User.findByLogin(params.login)
		if(u) {
			if(u.password == params.password) {
				session.user = u
				redirect(action:"home")
			}
			else {
				render(view:"login", model:[message:"Password incorrect"])							
			}
		}
		else {
			render(view:"login", model:[message:"User not found"])			
		}
	}
}

11.4 安全插件

如果你需要比简单认证更高级的功能,诸如授权(authorization),角色(roles)等,那么你可能要考虑使用一个可用的安全插件。

11.4.1 Acegi

Acegi插件是建立在 Spring Acegi 项目上,该项目为建立各种认证和授权架构提供了一个灵活,易扩展的框架。

Acegi插件需要你在URI和角色之间制定个详细的映射,为规范人,权威专家和请求maps提供一个默认的领域模型domain model。点击documentation on the wiki,查看更多信息。

11.4.2 JSecurity

JSecurity 是另外一个面向Java POJO的安全框架,它也可以提供一个规范领域,用户,角色和权限的默认领域模型。使用JSecurity,你必须让每个你想保护的controller类继承一个controller基类,然后提供一个建立角色的 accessControl代码块。例子如下:

class ExampleController extends JsecAuthBase {
    static accessControl = {
        // All actions require the 'Observer' role.
        role(name: 'Observer')

// The 'edit' action requires the 'Administrator' role. role(name: 'Administrator', action: 'edit')

// Alternatively, several actions can be specified. role(name: 'Administrator', only: [ 'create', 'edit', 'save', 'update' ]) }

… }

更多关于JSecurity的信息,参考JSecurity Quick Start